/*
 * $Id: BeanInfoSupport.java 4052 2011-07-19 20:17:22Z kschaefe $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.beans;

import java.awt.Image;
import java.beans.BeanDescriptor;
import java.beans.BeanInfo;
import java.beans.EventSetDescriptor;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.beans.SimpleBeanInfo;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.ImageIcon;

/**
 * Useful baseclass for BeanInfos. With this class, normal introspection occurs
 * and then you are given the opportunity to reconfigure portions of the bean
 * info in the <code>initialize</code> method.
 *
 * @author rbair, Jan Stola
 */
@SuppressWarnings("nls")
public abstract class BeanInfoSupport extends SimpleBeanInfo {
	private static Logger LOG = Logger.getLogger(BeanInfoSupport.class.getName());

	/**
	 * Indicates whether I am introspecting state for the give class. This helps
	 * prevent infinite loops
	 */
	private static Map<Class<?>, Boolean> introspectingState = new HashMap<Class<?>, Boolean>();
	/**
	 * The class of the bean that this BeanInfoSupport is for
	 */
	private Class<?> beanClass;

	/**
	 * @see BeanInfo
	 */
	private int defaultPropertyIndex = -1;
	/**
	 * @see BeanInfo
	 */
	private int defaultEventIndex = -1;
	/**
	 * The 16x16 color icon
	 */
	private Image iconColor16 = null;
	/**
	 * The 32x32 color icon
	 */
	private Image iconColor32 = null;
	/**
	 * The 16x16 monochrome icon
	 */
	private Image iconMono16 = null;
	/**
	 * The 32x32 monochrome icon
	 */
	private Image iconMono32 = null;
	/**
	 * A reference to the icon. This String must be of a form that ImageIO can
	 * use to locate and load the icon image
	 */
	private String iconNameC16 = null;
	/**
	 * A reference to the icon. This String must be of a form that ImageIO can
	 * use to locate and load the icon image
	 */
	private String iconNameC32 = null;
	/**
	 * A reference to the icon. This String must be of a form that ImageIO can
	 * use to locate and load the icon image
	 */
	private String iconNameM16 = null;
	/**
	 * A reference to the icon. This String must be of a form that ImageIO can
	 * use to locate and load the icon image
	 */
	private String iconNameM32 = null;

	private BeanDescriptor beanDescriptor;

	private Map<String, PropertyDescriptor> properties = new TreeMap<String, PropertyDescriptor>();
	private Map<String, EventSetDescriptor> events = new TreeMap<String, EventSetDescriptor>();
	private Map<String, MethodDescriptor> methods = new TreeMap<String, MethodDescriptor>();

	/**
	 * Creates a new instance of BeanInfoSupport.
	 * 
	 * @param beanClass
	 *            class of the bean.
	 */
	public BeanInfoSupport(Class<?> beanClass) {
		this.beanClass = beanClass;
		if (!isIntrospecting()) {
			introspectingState.put(beanClass, Boolean.TRUE);
			try {
				Class<?> superClass = beanClass.getSuperclass();
				while (superClass != null) {
					Introspector.flushFromCaches(superClass);
					superClass = superClass.getSuperclass();
				}
				BeanInfo info = Introspector.getBeanInfo(beanClass);
				beanDescriptor = info.getBeanDescriptor();
				if (beanDescriptor != null) {
					Class<?> customizerClass = getCustomizerClass();
					beanDescriptor = new BeanDescriptor(beanDescriptor.getBeanClass(),
							customizerClass == null ? beanDescriptor.getCustomizerClass() : customizerClass);
				} else {
					beanDescriptor = new BeanDescriptor(beanClass, getCustomizerClass());
				}
				for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
					properties.put(pd.getName(), pd);
				}
				for (EventSetDescriptor esd : info.getEventSetDescriptors()) {
					events.put(esd.getName(), esd);
				}
				for (MethodDescriptor md : info.getMethodDescriptors()) {
					methods.put(md.getName(), md);
				}

				defaultPropertyIndex = info.getDefaultPropertyIndex();
				defaultEventIndex = info.getDefaultEventIndex();

				iconColor16 = loadStandardImage(info, BeanInfo.ICON_COLOR_16x16);
				iconColor32 = loadStandardImage(info, BeanInfo.ICON_COLOR_32x32);
				iconMono16 = loadStandardImage(info, BeanInfo.ICON_MONO_16x16);
				iconMono32 = loadStandardImage(info, BeanInfo.ICON_MONO_32x32);
			} catch (Exception e) {
				e.printStackTrace();
			}
			introspectingState.put(beanClass, Boolean.FALSE);
			initialize();
		}
	}

	private boolean isIntrospecting() {
		Boolean b = introspectingState.get(beanClass);
		return b == null ? false : b.booleanValue();
	}

	/**
	 * attempts to load a png icon from the resource directory beneath beaninfo,
	 * named like: JXTaskPaneContainer16.png JXTaskPaneContainer16-mono.png
	 * JXTaskPaneContainer32.png JXTaskPaneContainer32-mono.png
	 * 
	 * if any of the icons is missing, an attempt is made to get an icon via
	 * introspection. If that fails, the icon will be set to placeholder16.png
	 * or one of the derivatives
	 */
	private Image loadStandardImage(BeanInfo info, int size) {
		String s = "";
		switch (size) {
			case BeanInfo.ICON_COLOR_16x16:
				s = "16";
				break;
			case BeanInfo.ICON_COLOR_32x32:
				s = "32";
				break;
			case BeanInfo.ICON_MONO_16x16:
				s = "16-mono";
				break;
			case BeanInfo.ICON_MONO_32x32:
				s = "32-mono";
				break;
		}
		String iconName = beanClass.getSimpleName() + s + ".png";

		Image image = null;
		try {
			image = loadImage("resources/" + iconName);
		} catch (Exception e) {
			LOG.info("No icon named " + iconName + " was found");
		}

		return image;
	}

	@Override
	public Image loadImage(final String resourceName) {
		URL url = getClass().getResource(resourceName);
		return (url == null) ? null : new ImageIcon(url).getImage();
	}

	/**
	 * Called by the constructor during the proper time so that subclasses can
	 * override the settings/values for the various beaninfo properties. For
	 * example, you could call setDisplayName("Foo Name", "foo") to change the
	 * foo properties display name
	 */
	protected abstract void initialize();

	/**
	 * Override this method if you want to return a custom customizer class for
	 * the bean
	 * 
	 * @return <code>null</code>.
	 */
	protected Class<?> getCustomizerClass() {
		return null;
	}

	// ------------------------------------ Methods for mutating the BeanInfo
	/**
	 * Specify the name/url/path to the small 16x16 color icon
	 * 
	 * @param name
	 *            name of the icon.
	 */
	protected void setSmallColorIconName(String name) {
		iconNameC16 = name;
	}

	/**
	 * Specify the name/url/path to the 32x32 color icon
	 * 
	 * @param name
	 *            name of the icon.
	 */
	protected void setColorIconName(String name) {
		iconNameC32 = name;
	}

	/**
	 * Specify the name/url/path to the small 16x16 monochrome icon
	 * 
	 * @param name
	 *            name of the icon.
	 */
	protected void setSmallMonoIconName(String name) {
		iconNameM16 = name;
	}

	/**
	 * Specify the name/url/path to the 32x32 monochrome icon
	 * 
	 * @param name
	 *            name of the icon.
	 */
	protected void setMonoIconName(String name) {
		iconNameM32 = name;
	}

	/**
	 * Changes the display name of the given named property. Property names are
	 * always listed last to allow for varargs
	 * 
	 * @param displayName
	 *            display name of the property.
	 * @param propertyName
	 *            name of the property.
	 */
	protected void setDisplayName(String displayName, String propertyName) {
		PropertyDescriptor pd = properties.get(propertyName);
		if (pd != null) {
			pd.setDisplayName(displayName);
		} else {
			LOG.log(Level.WARNING, "Failed to set display name for property '" + propertyName + "'. No such property was found");
		}
	}

	/**
	 * Sets the given named properties to be "hidden".
	 * 
	 * @param hidden
	 *            determines whether the properties should be marked as hidden
	 *            or not.
	 * @param propertyNames
	 *            name of properties.
	 * @see PropertyDescriptor
	 */
	protected void setHidden(boolean hidden, String... propertyNames) {
		for (String propertyName : propertyNames) {
			PropertyDescriptor pd = properties.get(propertyName);
			if (pd != null) {
				pd.setHidden(hidden);
			} else {
				LOG.log(Level.WARNING, "Failed to set hidden attribute for property '" + propertyName
						+ "'. No such property was found");
			}
		}
	}

	protected void setExpert(boolean expert, String... propertyNames) {
		for (String propertyName : propertyNames) {
			PropertyDescriptor pd = properties.get(propertyName);
			if (pd != null) {
				pd.setExpert(expert);
			} else {
				LOG.log(Level.WARNING, "Failed to set expert attribute for property '" + propertyName
						+ "'. No such property was found");
			}
		}
	}

	protected void setPreferred(boolean preferred, String... propertyNames) {
		for (String propertyName : propertyNames) {
			PropertyDescriptor pd = properties.get(propertyName);
			if (pd != null) {
				pd.setPreferred(preferred);
			} else {
				LOG.log(Level.WARNING, "Failed to set preferred attribute for property '" + propertyName
						+ "'. No such property was found");
			}
		}
	}

	protected void setBound(boolean bound, String... propertyNames) {
		for (String propertyName : propertyNames) {
			PropertyDescriptor pd = properties.get(propertyName);
			if (pd != null) {
				pd.setBound(bound);
			} else {
				LOG.log(Level.WARNING, "Failed to set bound attribute for property '" + propertyName
						+ "'. No such property was found");
			}
		}
	}

	protected void setConstrained(boolean constrained, String... propertyNames) {
		for (String propertyName : propertyNames) {
			PropertyDescriptor pd = properties.get(propertyName);
			if (pd != null) {
				pd.setConstrained(constrained);
			} else {
				LOG.log(Level.WARNING, "Failed to set constrained attribute for property '" + propertyName
						+ "'. No such property was found");
			}
		}
	}

	protected void setCategory(String categoryName, String... propertyNames) {
		for (String propertyName : propertyNames) {
			PropertyDescriptor pd = properties.get(propertyName);
			if (pd != null) {
				pd.setValue("category", categoryName);
			} else {
				LOG.log(Level.WARNING, "Failed to set category for property '" + propertyName + "'. No such property was found");
			}
		}
	}

	protected void setPropertyEditor(Class<?> editorClass, String... propertyNames) {
		for (String propertyName : propertyNames) {
			PropertyDescriptor pd = properties.get(propertyName);
			if (pd != null) {
				pd.setPropertyEditorClass(editorClass);
			} else {
				LOG.log(Level.WARNING, "Failed to set property editor for property '" + propertyName
						+ "'. No such property was found");
			}
		}
	}

	protected void setEnumerationValues(EnumerationValue[] values, String... propertyNames) {
		if (values == null) {
			return;
		}

		Object[] enumValues = new Object[values.length * 3];
		int index = 0;
		for (EnumerationValue ev : values) {
			enumValues[index++] = ev.getName();
			enumValues[index++] = ev.getValue();
			enumValues[index++] = ev.getJavaInitializationString();
		}

		for (String propertyName : propertyNames) {
			PropertyDescriptor pd = properties.get(propertyName);
			if (pd != null) {
				pd.setValue("enumerationValues", enumValues);
			} else {
				LOG.log(Level.WARNING, "Failed to set enumeration values for property '" + propertyName
						+ "'. No such property was found");
			}
		}
	}

	// ----------------------------------------------------- BeanInfo methods
	/**
	 * Gets the bean's <code>BeanDescriptor</code>s.
	 *
	 * @return BeanDescriptor describing the editable properties of this bean.
	 *         May return null if the information should be obtained by
	 *         automatic analysis.
	 */
	@Override
	public BeanDescriptor getBeanDescriptor() {
		return isIntrospecting() ? null : beanDescriptor;
	}

	/**
	 * Gets the bean's <code>PropertyDescriptor</code>s.
	 *
	 * @return An array of PropertyDescriptors describing the editable
	 *         properties supported by this bean. May return null if the
	 *         information should be obtained by automatic analysis.
	 *         <p>
	 *         If a property is indexed, then its entry in the result array will
	 *         belong to the IndexedPropertyDescriptor subclass of
	 *         PropertyDescriptor. A client of getPropertyDescriptors can use
	 *         "instanceof" to check if a given PropertyDescriptor is an
	 *         IndexedPropertyDescriptor.
	 */
	@Override
	public PropertyDescriptor[] getPropertyDescriptors() {
		return isIntrospecting() ? null : properties.values().toArray(new PropertyDescriptor[0]);
	}

	/**
	 * Gets the bean's <code>EventSetDescriptor</code>s.
	 *
	 * @return An array of EventSetDescriptors describing the kinds of events
	 *         fired by this bean. May return null if the information should be
	 *         obtained by automatic analysis.
	 */
	@Override
	public EventSetDescriptor[] getEventSetDescriptors() {
		return isIntrospecting() ? null : events.values().toArray(new EventSetDescriptor[0]);
	}

	/**
	 * Gets the bean's <code>MethodDescriptor</code>s.
	 *
	 * @return An array of MethodDescriptors describing the methods implemented
	 *         by this bean. May return null if the information should be
	 *         obtained by automatic analysis.
	 */
	@Override
	public MethodDescriptor[] getMethodDescriptors() {
		return isIntrospecting() ? null : methods.values().toArray(new MethodDescriptor[0]);
	}

	/**
	 * A bean may have a "default" property that is the property that will
	 * mostly commonly be initially chosen for update by human's who are
	 * customizing the bean.
	 * 
	 * @return Index of default property in the PropertyDescriptor array
	 *         returned by getPropertyDescriptors.
	 *         <P>
	 *         Returns -1 if there is no default property.
	 */
	@Override
	public int getDefaultPropertyIndex() {
		return isIntrospecting() ? -1 : defaultPropertyIndex;
	}

	/**
	 * A bean may have a "default" event that is the event that will mostly
	 * commonly be used by human's when using the bean.
	 * 
	 * @return Index of default event in the EventSetDescriptor array returned
	 *         by getEventSetDescriptors.
	 *         <P>
	 *         Returns -1 if there is no default event.
	 */
	@Override
	public int getDefaultEventIndex() {
		return isIntrospecting() ? -1 : defaultEventIndex;
	}

	/**
	 * This method returns an image object that can be used to represent the
	 * bean in toolboxes, toolbars, etc. Icon images will typically be GIFs, but
	 * may in future include other formats.
	 * <p>
	 * Beans aren't required to provide icons and may return null from this
	 * method.
	 * <p>
	 * There are four possible flavors of icons (16x16 color, 32x32 color, 16x16
	 * mono, 32x32 mono). If a bean choses to only support a single icon we
	 * recommend supporting 16x16 color.
	 * <p>
	 * We recommend that icons have a "transparent" background so they can be
	 * rendered onto an existing background.
	 *
	 * @param iconKind
	 *            The kind of icon requested. This should be one of the constant
	 *            values ICON_COLOR_16x16, ICON_COLOR_32x32, ICON_MONO_16x16, or
	 *            ICON_MONO_32x32.
	 * @return An image object representing the requested icon. May return null
	 *         if no suitable icon is available.
	 */
	@Override
	public java.awt.Image getIcon(int iconKind) {
		switch (iconKind) {
			case ICON_COLOR_16x16:
				return getImage(iconNameC16, iconColor16);
			case ICON_COLOR_32x32:
				return getImage(iconNameC32, iconColor32);
			case ICON_MONO_16x16:
				return getImage(iconNameM16, iconMono16);
			case ICON_MONO_32x32:
				return getImage(iconNameM32, iconMono32);
			default:
				return null;
		}
	}

	private java.awt.Image getImage(String name, java.awt.Image img) {
		if (img == null) {
			if (name != null) {
				img = loadImage(name);
			}
		}
		return img;
	}
}
